﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Ccr.Core;
using XcoAppSpaces.Contracts.CcrDispatching;
using XcoAppSpaces.Contracts.CcrExtensions;
using XcoAppSpaces.Contracts.Communication;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Service;
using XcoAppSpaces.Contracts.Transport;
using XcoAppSpaces.Contracts.WorkerResolve;
using XcoAppSpaces.Core.Communication.WorkerResolve;
using XcoAppSpaces.Contracts.Logging;
using XcoAppSpaces.Contracts.Security;
using System.Net;
using XcoAppSpaces.Contracts.Messaging;
using XcoAppSpaces.Contracts.Worker;
using XcoAppSpaces.Contracts.Serialization;
using System.Reflection;

namespace XcoAppSpaces.Core.Communication
{
	/// <summary>
	/// Remote communication component for AppSpace. Handles the transport of messages
	/// from/to remote workers and ports.
	/// </summary>
	public class Connector : IConnector
	{
		private readonly IXcoLogger log = null;
		private readonly CommunicationEvents communicationEvents;
		private MessageTransmitter messageTransmitter = null;
		private ResolveWorker resolver = null;
		private IXcoWorkerRegistry localWorkers = null;
		
		/// <summary>
		/// Creates a new Connector.
		/// </summary>
		public Connector()
		{
			this.log = XcoLoggerFactory.CreateInstance(this.GetType());
			this.communicationEvents = new CommunicationEvents();
		}

		/// <summary>
		/// Initializes the connector instance and gets all other needed services from the service registry.
		/// </summary>
		/// <param name="serviceRegistry">The service registry of the current appspace instance.</param>
		public void Initialize(IXcoServiceRegistry serviceRegistry)
		{
			this.messageTransmitter = new MessageTransmitter(serviceRegistry, communicationEvents);
			//serviceRegistry.Register<IXcoMessageReceiverService>(messageTransmitter);

			localWorkers = serviceRegistry.Resolve<IXcoWorkerRegistry>();
			this.resolver = new ResolveWorker(
				localWorkers,
				serviceRegistry.Resolve<IXcoSecurityService>(), 
				messageTransmitter.SendMessage,
				serviceRegistry.Resolve<ICcrDispatcherService>().InternalDispatcherQueue,
				communicationEvents);
			localWorkers.Register<ResolveWorkerContract>(this.resolver, null);
		}

		/// <summary>
		/// The local address of the space (in form hostname:port).
		/// </summary>
		public string LocalAddress
		{
			get { return messageTransmitter.LocalAddress; }
		}
        	
		/// <summary>
		/// Checks a certain address if it is the address of the local space. This is the case if the
		/// address is either NULL of if it is equal (ignoring case) to the local address.
		/// </summary>
		/// <param name="address">The address to be checked.</param>
		/// <returns>True if the address refers to the local space.</returns>
		public bool IsLocal(string address)
		{
			return (address == null || address.Equals(messageTransmitter.LocalAddress, StringComparison.OrdinalIgnoreCase));
		}

		/// <summary>
		/// The list of registered communication services.
		/// </summary>
		public IEnumerable<XcoServiceConfiguration<IXcoTransportService>> CommServices
		{
			get { return messageTransmitter.CommServiceInfo; }
		}

		/// <summary>
		/// Port where communication errors are posted that are not caught by causalities.
		/// </summary>
		public Port<XcoCommunicationError> Errors
		{
			get { return messageTransmitter.Errors; }
		}

		/// <summary>
		/// Events giving information about incoming and outgoing communication, e.g. for monitoring
		/// </summary>
		public ICommunicationEvents CommunicationEvents
		{
			get { return communicationEvents; }
		}

		/// <summary>
		/// Establishes a connection to a worker that is running at a remote space. Creates a local
		/// proxy of the worker that allows posting messages that are automatically transported
		/// to the worker.
		/// If the worker does't exist or the connection to the space cannot be established, an
		/// XcoCommunicationException is thrown.
		/// This method is an alternative to the method taking an URI as address, being more flexible
		/// and therefore allowing things like Jabber usernames to be used.
		/// </summary>
		/// <typeparam name="TContract">The contract type of the worker to connect to.</typeparam>
		/// <param name="workerAddress">The address of the worker in den form "address/workername"</param>
		/// <param name="commServiceName">The name of the communication service that should be used to communicate with the worker (or null if the default communication service should be used).</param>
		/// <param name="credentials">The credentials that should be used to connect to the worker.</param>
		/// <returns>A worker contract instance that allows to post messages to the remote worker.</returns>
		public WorkerProxyInfo ConnectWorker<TContract>(string workerAddress, string commServiceName, ICredentials credentials)
			where TContract : IPort, new()
		{
			//split worker address into address and name: everything behind last "/" is the worker name
			string name;
			string address;
			SplitWorkerAddress(workerAddress, out address, out name);

			return ConnectWorker<TContract>(name, address, commServiceName, credentials);
		}

		/// <summary>
		/// Establishes a connection to a worker that is running at a remote space. Creates a local
		/// proxy of the worker that allows posting messages that are automatically transported
		/// to the worker.
		/// If the worker does't exist or the connection to the space cannot be established, an
		/// XcoCommunicationException is thrown.
		/// </summary>
		/// <typeparam name="TContract">The contract type of the worker to connect to.</typeparam>
		/// <param name="name">The name of the worker.</param>
		/// <param name="address">The address of the space where the worker is running (in form hostname:port).</param>
		/// <param name="commServiceName">The name of the communication service that should be used to communicate with the worker (or null if the default communication service should be used).</param>
		/// <param name="credentials">The credentials that should be used to connect to the worker.</param>
		/// <returns>A worker contract instance that allows to post messages to the remote worker.</returns>
        public WorkerProxyInfo ConnectWorker<TContract>(string name, string address, string commServiceName, ICredentials credentials)
			where TContract : IPort, new()
		{
			//check if transport service exists
			var commServiceInfo = messageTransmitter.GetCommServiceInfo(commServiceName);

			//check if serializer can serialize the worker message types
			var serializer = messageTransmitter.GetSerializer(commServiceInfo.Name);
			serializer.CheckIfMessageTypesCanBeSerialized<TContract>();

			//check if remote worker exists and can be accessed
			var workerIdentifier = localWorkers.GenerateIdentifier(typeof(TContract), name);
			ResolveResponse resolve = resolver.Resolve(address, workerIdentifier, commServiceInfo.Name, credentials);
			CheckWorkerResolveResponse(address, typeof(TContract), resolve);

			//create proxy
			return new WorkerProxyInfo(typeof(TContract), name, address, workerIdentifier, commServiceInfo.Name, resolve.SessionID, resolve.ProcessorAttributes, messageTransmitter.SendMessage);
		}


		//Helper method for unittesting - allows creating worker proxies without the need of an existing server
		//this helps when testing message transfer error handling
		internal WorkerProxyInfo CreateUncheckedWorkerProxy<TContract>(string name, string address, string commServiceName)
			where TContract : IPort, new()
		{
			var commServiceInfo = messageTransmitter.GetCommServiceInfo(commServiceName);
			var workerIdentifier = localWorkers.GenerateIdentifier(typeof(TContract), name);
			
			//create proxy
			return new WorkerProxyInfo(typeof(TContract), name, address, workerIdentifier, commServiceInfo.Name, Guid.Empty, null, messageTransmitter.SendMessage);
		}

		

		internal void SplitWorkerAddress(string workerAddress, out string address, out string workerName)
		{
			//split worker address into address and name: everything behind last "/" is the worker name
			int index = workerAddress.LastIndexOf('/');
			if (index == -1)
			{
				address = workerAddress;
				workerName = null;
			}
			else
			{
				address = workerAddress.Substring(0, index);
				workerName = workerAddress.Substring(index + 1);
				if (workerName.Length == 0)
					workerName = null;
			}
		}

		private void CheckWorkerResolveResponse(string workerAddress, Type workerType, ResolveResponse resolve)
		{
			switch (resolve.Status)
			{
				case ResolveStatus.Ok:
					break;

				case ResolveStatus.NotAuthenticated:
				case ResolveStatus.NotAuthorized:
					log.Warn("Worker at " + workerAddress + " with type " + workerType.Name + " could not be resolved. Reason: " + resolve.Status);
					throw new XcoSecurityException("Worker at " + workerAddress + " with type " + workerType.Name + " could not be accessed. Reason: " + resolve.Status);

				case ResolveStatus.WorkerNotFound:
				case ResolveStatus.NoResponse:
				default:
					log.Warn("Worker at " + workerAddress + " with type " + workerType.Name + " could not be resolved. Reason: " + resolve.Status);
					throw new XcoCommunicationException("Worker at " + workerAddress + " with type " + workerType.Name + " could not be resolved. Reason: " + resolve.Status + 
						(resolve.Error != null ? ("(" + resolve.Error.Message + ")") : ""), resolve.Error);
			}
		}

		/// <summary>
		/// Disposes the connector.
		/// </summary>
		public void Dispose()
		{
			messageTransmitter.Dispose();
		}

		#region IXcoService Members

		/// <summary>
		/// Starts the connector.
		/// </summary>
		public void Start()
		{
			messageTransmitter.Start();
		}

		/// <summary>
		/// Stops the connector.
		/// </summary>
		public void Stop()
		{
			Dispose();
		}

		/// <summary>
		/// Start priority of the connector (needs to be started before the communication services).
		/// </summary>
		public int StartPriority
		{
			get { return 1; } 
		}

		#endregion
	}
}
